package com.gitee.wsl.net.server.ratelimiting


import com.gitee.wsl.struct.generator.randomUUID
import com.gitee.wsl.time.fromMinutes
import kotlinx.atomicfu.atomic
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.Clock
import kotlinx.datetime.Clock.System.now
import kotlinx.datetime.Instant
import kotlin.test.Test
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes


class LinearRateCountLimiterTest {

    fun interface NowProvider {
        fun now(): Instant
    }

   /* private fun timeProvider(mockNow: Instant) = mockk<NowProvider> {
        every { now() } returns mockNow
    }*/

    @Test
    fun `test hit the request rate then reset and then hit again`() {
        val limitId = randomUUID()

        val limit = 10L
        val window = Duration.fromMinutes(10)
        val now = now()
        val timeProvider = NowProvider{now()}
        val diffSeconds = window.inWholeSeconds

        val hostName = "hostUnderTheTest"

        val instance = LinearRateLimiter(
            limitersSettings = mapOf(limitId to Pair(limit, window)),
            nowProvider = { timeProvider.now() },
        )

        val workers = 10
        val repetitionsPerWorker = 50
        // just to check the parameters, that we can really get denied result
        assertTrue {
            @Suppress("KotlinConstantConditions") // because we sometimes change when testing
            limit < (workers + repetitionsPerWorker)
        }
        instance.stressTest(
            limitId = limitId,
            workers = workers,
            repetitionsPerWorker = repetitionsPerWorker,
            hostName = hostName,
            limit = limit,
            diffSeconds = diffSeconds
        )
        // verify that the host is still locked out
        assertEquals(diffSeconds, instance.processRequest(limitId, hostName))
        // trigger the reset
        val newNow = now.plus(window+1.minutes)
        //every { timeProvider.now() } returns newNow
        // verify that the host is now free to run the requests again
        instance.stressTest(
            limitId = limitId,
            workers = workers,
            repetitionsPerWorker = repetitionsPerWorker,
            hostName = hostName,
            limit = limit,
            diffSeconds = diffSeconds
        )
        // verify that the host is now locked out again
        assertEquals(diffSeconds, instance.processRequest(limitId, hostName))
    }

    @Suppress("LongParameterList") // this is a test, we're fine
    private fun LinearRateLimiter.stressTest(
        limitId: String,
        workers: Int,
        repetitionsPerWorker: Int,
        hostName: String,
        limit: Long,
        diffSeconds: Long
    ) {
        val limiter = this
        val counter = atomic(0)
        // execute test that will lock our host down
        runBlocking {
            repeat(workers / 2) {
                // measured host
                launch {
                    repeat(repetitionsPerWorker) {
                        val nextTime = counter.incrementAndGet()
                        val result = limiter.processRequest(limitId, hostName)
                        if (nextTime > limit) {
                            assertEquals(diffSeconds, result)
                        } else {
                            assertNull(result)
                        }
                    }
                }
                // random hosts must have an access
                launch {
                    repeat(repetitionsPerWorker) {
                        assertNull(limiter.processRequest(limitId, randomUUID()))
                    }
                }
            }
        }
    }
}
